﻿
using Boilen.Primitives;
using BoilenEditor.Primitives;
using BoilenEditor.Properties;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Threading;
using Xml = System.Xml.Linq;

namespace BoilenEditor {

    public partial class OpenWindow : Window {

        private const string BoilenFileTemplate = @"<#@ include file=""{0}"" #>
<#@ include file=""{1}"" #>
<#
Partial.Type<{2}>( )
    .Run( this.Write );
#>
";
        private const string ParentFileTemplate = @"
using System;
using System.Windows;

namespace {3}
{{
    public partial class {2} : DependencyObject // TODO: UPDATE AND BUILD WPF
    {{
    }}
}}
";

        private static readonly FrameworkElement EmptyFrameworkElement = new FrameworkElement( );

        private readonly DispatcherTimer searchDelay_;
        private readonly OpenData openData_;
        private OpenFileDialog browseSolution_;


        public OpenWindow( Window owner, OpenData openData ) {
            this.Owner = owner;
            this.ShowInTaskbar = owner == null;

            this.searchDelay_ = new DispatcherTimer( TimeSpan.FromMilliseconds( 250 ), DispatcherPriority.Background, this.OnSearchDelayTick, this.Dispatcher ) { IsEnabled = false };
            this.openData_ = openData;
            this.DataContext = openData;

            InitializeComponent( );

            this.FileItems.GroupDescriptions.Add( new PropertyGroupDescription( "Group" ) );
            this.FileItems.SortDescriptions.Add( new SortDescription( "IsBoilenFile", ListSortDirection.Descending ) );
            this.FileItems.SortDescriptions.Add( new SortDescription( "SearchString", ListSortDirection.Ascending ) );

            this.Loaded += delegate {
                this.UpdateProjectSelection( );
                this.OnSearchDelayTick( this, EventArgs.Empty );
            };
        }


        public IEnumerable<ProjectData> SelectedProjects {
            get { return this.projectListBox_.SelectedItems.Cast<ProjectData>( ); }
        }

        public FileData SelectedFile {
            get { return (FileData)this.fileListBox_.SelectedItem; }
            set { this.fileListBox_.SelectedItem = value; }
        }

        private OpenFileDialog BrowseSolution {
            get {
                if( !this.browseSolution_.HasValue( ) )
                    this.browseSolution_ = new OpenFileDialog {
                        InitialDirectory = this.openData_.SolutionDirectory,
                        CheckFileExists = true,
                        Filter = "Solution Files (*.sln)|*.sln"
                    };

                return this.browseSolution_;
            }
        }

        private ItemCollection FileItems {
            get { return this.fileListBox_.Items; }
        }


        private void BrowseForSolution( ) {
            var dialog = this.BrowseSolution;
            bool? result = dialog.ShowDialog( );
            if( result.GetValueOrDefault( ) ) {
                this.openData_.SolutionPath = dialog.FileName;
                this.projectListBox_.SelectAll( );
            }
        }

        private void UpdateProjectSelection( ) {
            if( this.projectListBox_.SelectedItems.Count == 0 )
                this.projectListBox_.SelectAll( );
        }

        private void Refresh( string filter, bool includeEmpty ) {
            this.filterBox_.Text = filter;
            if( Settings.Default.ProjectDirectoryFilter != filter ) {
                Settings.Default.ProjectDirectoryFilter = filter;
                Settings.Default.Save( );
            }

            this.openData_.ProjectDirectoryFilter = filter;
            this.openData_.IncludeEmptyProjects = includeEmpty;
            this.Dispatcher.BeginInvoke( new Action( this.UpdateProjectSelection ) );
            this.OnFilterChanged( this, null );
        }

        private void SelectFile( ) {
            this.DialogResult = true;
        }

        private void CreateFile( ) {
            string sourceFilePath = this.GetFilePath( );
            string generatedFilePath = Path.ChangeExtension( sourceFilePath, ".cs" );
            string parentFilePath = generatedFilePath.Replace( ".b.cs", ".cs" );
            bool parentFileExists = File.Exists( parentFilePath );

            string[] files =
                parentFileExists
                    ? new[] { sourceFilePath, generatedFilePath }
                    : new[] { sourceFilePath, generatedFilePath, parentFilePath + " (UPDATE AND REBUILD WPF DEBUG)" };

            ProjectData selectedProject = (ProjectData)this.projectListBox_.SelectedItem;
            string[] projectPaths =
                Ex.GetFiles( selectedProject.ProjectDirectory, "*.csproj", SearchOption.TopDirectoryOnly )
                  .Where( path => Path.GetFileName( path ).Replace( ".SL", "" ).OrdinalEqual( selectedProject.Name ) )
                  .OrderBy( path => path.Length )
                  .ToArray( );

            string sourceFileDirectory = Path.GetDirectoryName( sourceFilePath );
            string referencesSearchDirectory = this.openData_.SolutionDirectory;
            string boilenReferenceFile = FindRelativePath( referencesSearchDirectory, "Boilen.tt", sourceFileDirectory );
            string includesReferenceFile = FindRelativePath( referencesSearchDirectory, "Includes.tt", sourceFileDirectory );

            string message = string.Format(
                "Do you want to create this new Boilen file?{0}{0}File Names:{0}  {1}{0}{0}Directory{2}:{0}\u00A0\u00A0{3}{0}{0}References:{0}  {4}{0}{0}Projects:{0}  {5}",
                Environment.NewLine,
                ListPaths( files ),
                Directory.Exists( sourceFileDirectory ) ? "" : " (NEW)",
                sourceFileDirectory,
                ListPaths( new[] { boilenReferenceFile, includesReferenceFile, } ),
                ListPaths( projectPaths )
            );

            MessageBoxResult result = MessageBox.Show( message, "Confirm File Creation", MessageBoxButton.YesNo, MessageBoxImage.Question );
            if( result == MessageBoxResult.Yes ) {
                var projectDocuments = Array.ConvertAll( projectPaths, Xml.XDocument.Load );

                Xml.XNamespace ns = projectDocuments[0].Root.Attribute( "xmlns" ).Value;
                var n = new {
                    // elements
                    Compile = ns + "Compile",
                    DependentUpon = ns + "DependentUpon",
                    Generator = ns + "Generator",
                    ItemGroup = ns + "ItemGroup",
                    LastGenOutput = ns + "LastGenOutput",
                    None = ns + "None",
                    PropertyGroup = ns + "PropertyGroup",
                    RootNamespace = ns + "RootNamespace",
                    // attributes
                    Include = "Include"
                };

                string projectNamespace =
                    projectDocuments[0].Root
                        .Elements( n.PropertyGroup )
                        .SelectMany( e => e.Elements( n.RootNamespace ) )
                        .Single( )
                        .Value;

                Func<string, string> getTrimmedRelativePath = path => GetRelativePath( selectedProject.ProjectDirectory, path ).TrimStart( '.', Path.DirectorySeparatorChar );
                string parentFileRelativePath = getTrimmedRelativePath( parentFilePath );
                string parentFileNamespace =
                    Path.GetDirectoryName( parentFileRelativePath )
                        .Split( Path.DirectorySeparatorChar )
                        .Where( s => s.Length > 0 )
                        .Aggregate( projectNamespace, ( l, r ) => l + "." + r );

                bool success = true;
                FileData boilenFileData = null;
                var createdFiles = new List<string>( );

                var filesToCreate = new[] {
                    new { FilePath = sourceFilePath, Template = BoilenFileTemplate },
                    new { FilePath = generatedFilePath, Template = "" },
                    parentFileExists ? null : new { FilePath = parentFilePath, Template = ParentFileTemplate }
                };
                foreach( var file in filesToCreate.Where( file => file.HasValue( ) ) ) {
                    string path = file.FilePath;
                    bool exists = File.Exists( path );
                    FileData fileData = new FileData( path, selectedProject.RootDirectory, selectedProject );
                    if( fileData.IsBoilenFile )
                        boilenFileData = fileData;

                    FileContents fileContents = new FileContents( fileData );
                    fileContents.Content = string.Format( file.Template, boilenReferenceFile, includesReferenceFile, Path.GetFileNameWithoutExtension( parentFilePath ), parentFileNamespace );

                    success =
                           fileContents.Save( )
                        && (exists || Ex.CallSourceControlTool( path, Ex.SourceControlAction.Add ));

                    if( success )
                        createdFiles.Add( path );
                    else
                        break;
                }

                for( int i = 0; success && i < projectPaths.Length; ++i ) {
                    string path = projectPaths[i];
                    var doc = projectDocuments[i];
                    bool isSilverlight = Path.GetFileName( path ).Contains( ".SL" );

                    Xml.XElement parentElement;
                    if( parentFileExists ) {
                        parentElement =
                            doc.Root.Elements( n.ItemGroup )
                               .SelectMany( ig => ig.Elements( n.Compile ) )
                               .Single( e => e.Attribute( n.Include ).Value.OrdinalEqual( parentFileRelativePath ) );
                    }
                    else {
                        Xml.XElement itemGroup =
                            doc.Root.Elements( n.ItemGroup )
                               .First( ig => ig.Elements( n.Compile ).Any( ) );
                        parentElement = new Xml.XElement( n.Compile, new Xml.XAttribute( n.Include, parentFileRelativePath ) );
                        itemGroup.Add( parentElement );
                    }

                    Xml.XElement generatedElementParent;
                    if( isSilverlight ) {
                        generatedElementParent = parentElement;
                    }
                    else {
                        generatedElementParent = new Xml.XElement( n.None,
                            new Xml.XAttribute( n.Include, getTrimmedRelativePath( sourceFilePath ) ),
                            new Xml.XElement( n.Generator, "TextTemplatingFileGenerator" ),
                            new Xml.XElement( n.LastGenOutput, Path.GetFileName( generatedFilePath ) ),
                            new Xml.XElement( n.DependentUpon, Path.GetFileName( parentFileRelativePath ) )
                        );
                        parentElement.AddAfterSelf( generatedElementParent );
                    }

                    var generatedElement = new Xml.XElement( n.Compile,
                        new Xml.XAttribute( n.Include, getTrimmedRelativePath( generatedFilePath ) ),
                        new Xml.XElement( n.DependentUpon, Path.GetFileName( generatedElementParent.Attribute( n.Include ).Value ) )
                    );
                    generatedElementParent.AddAfterSelf( generatedElement );


                    FileData projectData = new FileData( path, selectedProject.RootDirectory, null );
                    FileContents projectContents = new FileContents( projectData );
                    projectContents.Content = @"<?xml version=""1.0"" encoding=""utf-8""?>" + Environment.NewLine + doc;

                    success = projectContents.Save( );
                }

                if( success ) {
                    selectedProject.TemplateFiles.Add( boilenFileData );
                    this.fileListBox_.ItemsSource = null;
                    this.fileListBox_.Items.Add( boilenFileData );
                    this.SelectedFile = boilenFileData;
                    this.SelectFile( );
                }
                else {
                    createdFiles.ForEach( File.Delete );
                }
            }
        }

        private static string GetRelativePath( string compareDirectory, string path ) {
            string[] sourceDirectories = compareDirectory.Split( Path.DirectorySeparatorChar );
            string[] pathDirectories = path.Split( Path.DirectorySeparatorChar );

            string separator = Path.DirectorySeparatorChar.ToString( );
            int lastCommonIndex =
                Enumerable.Range( 0, Math.Min( sourceDirectories.Length, pathDirectories.Length ) )
                    .TakeWhile( i => sourceDirectories[i].OrdinalEqual( pathDirectories[i] ) )
                    .Last( );

            int upCount = sourceDirectories.Length - lastCommonIndex - 1;
            string upString = upCount == 0 ? "." : Util.Join( Enumerable.Repeat( "..", upCount ), separator );

            int downStart = lastCommonIndex + 1;
            string downString = Util.Join( pathDirectories.Skip( downStart ), separator );

            string relativePath = Path.Combine( upString, downString );
            return relativePath;
        }

        private static string FindRelativePath( string rootDirectory, string fileName, string compareDirectory ) {
            string path = null;

            string searchDirectory = compareDirectory;
            while( path == null && searchDirectory.Length > rootDirectory.Length ) {
                path = Ex.GetFiles( searchDirectory, fileName, SearchOption.TopDirectoryOnly ).FirstOrDefault( );
                searchDirectory = Path.GetDirectoryName( searchDirectory );
            }

            path = path ?? Ex.GetFiles( rootDirectory, fileName ).FirstOrDefault( );

            string relativePath =
                path.HasValue( )
                    ? GetRelativePath( compareDirectory, path )
                    : "Could not find reference " + fileName;
            return relativePath;
        }

        private static string ListPaths( IEnumerable<string> paths ) {
            var simplifiedPaths = paths.Select( path => Path.IsPathRooted( path ) ? Path.GetFileName( path ) : path );
            string list = string.Join( Environment.NewLine + "  ", simplifiedPaths );
            return list;
        }


        private void BrowseExecuted( object sender, ExecutedRoutedEventArgs e ) {
            this.BrowseForSolution( );
        }

        private void RefreshExecuted( object sender, ExecutedRoutedEventArgs e ) {
            this.Refresh( this.filterBox_.Text, this.includeEmpty_.IsChecked.Value );
        }

        private void SelectCanExecute( object sender, CanExecuteRoutedEventArgs e ) {
            if( this.fileListBox_.HasValue( ) && this.SelectedFile.HasValue( ) )
                e.CanExecute = true;
        }

        private void SelectExecuted( object sender, ExecutedRoutedEventArgs e ) {
            this.SelectFile( );
        }

        private void CreateCanExecute( object sender, CanExecuteRoutedEventArgs e ) {
            string testPath = this.GetFilePath( );
            if( Ex.IsValidPath( testPath ) && !File.Exists( testPath ) )
                e.CanExecute = true;
        }

        private void CreateExecuted( object sender, ExecutedRoutedEventArgs e ) {
            this.CreateFile( );
        }

        private void PropertiesExecuted( object sender, ExecutedRoutedEventArgs e ) {
            var dialog = new SettingsWindow( this );
            bool? result = dialog.ShowDialog( );
            bool success = result.GetValueOrDefault( );

            if( success ) {
                WindowSettings ws = Settings.Default.OpenWindowSettings;
                ws.Apply( this );

                if( this.filterBox_.Text != Settings.Default.ProjectDirectoryFilter )
                    this.Refresh( Settings.Default.ProjectDirectoryFilter, this.openData_.IncludeEmptyProjects );
            }
        }

        private void CloseExecuted( object sender, ExecutedRoutedEventArgs e ) {
            this.Close( );
        }


        private void ProjectSelectionChanged( object sender, SelectionChangedEventArgs e ) {
            this.fileListBox_.ItemsSource = this.SelectedProjects.SelectMany( project => project.TemplateFiles );
        }

        private void OnSearchDelayTick( object sender, EventArgs e ) {
            this.searchDelay_.Stop( );

            this.FileItems.Filter = this.GetFilter( this.searchBox_.Text );
            if( this.FileItems.IsEmpty && string.IsNullOrWhiteSpace( this.searchBox_.Text ) )
                this.FileItems.Filter = null;

            if( this.fileListBox_.Items.Count == 1 )
                this.SelectedFile = (FileData)this.fileListBox_.Items[0];
            CommandManager.InvalidateRequerySuggested( );
        }

        private void OnSearchTextChanged( object sender, TextChangedEventArgs e ) {
            // (Re)start the search delay, until the user pauses typing.
            this.searchDelay_.Stop( );
            this.searchDelay_.Start( );
        }

        private void OnFilterChanged( object sender, RoutedEventArgs e ) {
            if( this.refreshButton_ == null )
                return;

            bool change =
                   this.includeEmpty_.IsChecked != this.openData_.IncludeEmptyProjects
                || this.filterBox_.Text != this.openData_.ProjectDirectoryFilter;
            this.refreshButton_.FontWeight =
                change
                    ? FontWeights.Bold
                    : FontWeights.Normal;
        }

        private Predicate<object> GetFilter( string text ) {
            string[] searches =
                (text ?? "").Split( )
                    .Where( search => !string.IsNullOrEmpty( search ) )
                    .ToArray( );

            Predicate<FileData> filter;
            if( searches.Any( ) )
                filter = file => searches.All( search => file.SearchString.IndexOf( search, StringComparison.OrdinalIgnoreCase ) >= 0 );
            else if( this.projectListBox_.SelectedItems.Count == this.projectListBox_.Items.Count )
                filter = file => file.IsBoilenFile;
            else
                filter = null;

            return filter == null
                 ? default( Predicate<object> )
                 : obj => filter( (FileData)obj );
        }

        private string GetFilePath( ) {
            if( !this.searchBox_.HasValue( ) )
                return null;

            string filePath =
                this.SelectedProjects
                    .Select( project => GetFilePath( project.ProjectDirectory, this.searchBox_.Text ) )
                    .FirstOrDefault( file => !string.IsNullOrEmpty( file ) );
            return filePath;
        }

        private static string GetFilePath( string solutionDirectory, string text ) {
            try {
                string directory = Path.GetDirectoryName( text );
                string name = Path.GetFileName( text );
                if( !name.EndsWith( ".tt" ) ) {
                    if( !name.EndsWith( ".b" ) ) { name += ".b"; }
                    name += ".tt";
                }

                string enteredPath = Path.Combine( directory, name );
                string fullPath = Path.Combine( solutionDirectory, enteredPath );
                return Path.GetFullPath( fullPath );
            }
            catch( NullReferenceException ) {
                return "";
            }
            catch( ArgumentException ) {
                return "";
            }
        }

        private void FileListBoxDoubleClick( object sender, MouseButtonEventArgs e ) {
            var fileDataElement = e.OriginalSource as FrameworkElement ?? EmptyFrameworkElement;
            var fileData = fileDataElement.DataContext as FileData;

            if( fileData.HasValue( ) && fileData == this.SelectedFile )
                this.SelectFile( );
        }

        private void SearchBoxPreviewKeyDown( object sender, KeyEventArgs e ) {
            bool canNavigate =
                   e.Key == Key.Down
                && e.KeyboardDevice.Modifiers == ModifierKeys.None
                && this.FileItems.Count > 0
                && Keyboard.FocusedElement == this.searchBox_;

            FrameworkElement focusedElement = this.searchBox_;
            while( canNavigate && focusedElement.MoveFocus( new TraversalRequest( FocusNavigationDirection.Next ) ) ) {
                focusedElement = (FrameworkElement)Keyboard.FocusedElement;

                var item = focusedElement as ListBoxItem;
                if( item.HasValue( ) ) {
                    item.IsSelected = true;
                    e.Handled = true;
                    break;
                }
            }
        }

    }

}
